package typejump.engine;

import java.awt.Color;
import java.awt.geom.Point2D;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import typejump.Messenger;
import typejump.Settings;
import typejump.Swagger;
import typejump.TypeJump;
import typejump.commands.TextCommand;
import typejump.objects.GameObject;
import typejump.objects.Actor;
import typejump.objects.ObjectGroup;
import typejump.objects.Player;
import typejump.objects.TextEffect;
import typejump.util.NestedList;

/**
 * This is the class which drives the main loop of the game and serves as an
 * interface for object, message, and command management.
 * 
 * @author g
 * 
 */
public class GameEngine {

	private Thread gameThread;

	private Hashtable<OnGoingThing, List<GameEvent>> endEvents = new Hashtable<OnGoingThing, List<GameEvent>>();
	
	private LinkedList<GameObject> background = new LinkedList<GameObject>();
	private LinkedList<GameObject> objects = new LinkedList<GameObject>();
	private LinkedList<GameObject> effects = new LinkedList<GameObject>();

	private NestedList<GameObject> allObjects = new NestedList<GameObject>();

	private Hashtable<String, GameObject> objectTable = new Hashtable<String, GameObject>();

	private final Object objectLockSecret = new Object();

	private CommandManager commands = new CommandManager();

	private LinkedList<OnGoingThing> onGoingThingList = new LinkedList<OnGoingThing>();

	private boolean blockRegenFlag = false;
	private int blockRegenFor = 0;
	
	private volatile double translateX = 0, translateY = 0;
	private volatile boolean focusOnPlayer = true;
	
	private volatile boolean paused = false;
	
	private Color backgroundC = Color.WHITE, foregroundC = Color.BLACK;

	private Player player;

	private static GameEngine me = new GameEngine();
	
	private ObjectGroup selectedObjects = new ObjectGroup();
	
	private double initialPlayerX, initialPlayerY;

	public static GameEngine getInst() {
		return me;
	}

	public GameEngine() {
		allObjects.add(background);
		allObjects.add(objects);
		allObjects.add(effects);

		gameThread = new Thread() {
			@Override
			public void run() {
				mainLoop();
			}
		};
	}
	
	public static Object showMessage(String message, int x, int y) {
		return null;
	}
	
	public static void hideMessage(Object id) {
		
	}
	
	public static GameObject getSelection() {
		return me.selectedObjects;
	}
	
	public static void deleteSelected() {
		me.selectedObjects.deleteContents();
	}
	
	public static void select(GameObject object) {
		me.selectedObjects.add(object);
	}
	public static void deselect(GameObject object) {
		me.selectedObjects.remove(object);
	}
	public static void deselectAll() {
		me.selectedObjects.clear();
	}
	
	public static void setInitialPlayerPosition() {
		if (me.player != null) {
			me.initialPlayerX = me.player.centerX();
			me.initialPlayerY = me.player.centerY();
		}
	}
	
	public static void resetPlayerPosition() {
		if (me.player != null) {
			me.player.centerX(me.initialPlayerX);
			me.player.centerY(me.initialPlayerY);
		}		
	}
	
	
	public static void setPaused(boolean pause) {
		me.paused = pause;
	}
	
	public static boolean isPaused() {
		return me.paused;
	}
	
	public static Color getBackgroundColor() { return me.backgroundC; }
	public static Color getForegroundColor() { return me.foregroundC; }
	public static void setBackgroundColor(Color c) { me.backgroundC = c; }
	public static void setForegroundColor(Color c) { me.foregroundC = c; }
	
	private static boolean tryPass(Object on, Object obj) {
		try {
			Method m = on.getClass().getMethod("pass", Object.class);
			m.invoke(on, obj);
			return true;
		} catch (Exception e) {
			
		}
		return false;
	}
	public static void pass(String to, Object obj) {
		synchronized (objectLock("Passing obj to " + to)) {
			for (OnGoingThing on : me.happenedThings) {
				if (on.getClass().getName().endsWith("." + to)) {
					if (tryPass(on, obj)) return;
				}
			}
			for (TextCommand tc : me.commands.commands()) {
				if (tc.getClass().getName().endsWith("." + to)) {
					if (tryPass(tc, obj)) return;
				}
			}
		}
	}
	
	public static void addEndEvent(OnGoingThing to, Object obj, String method) {
		if (obj == null || method == null)
			return;
		GameEvent event = new GameEvent(obj, method);
		if (me.endEvents.containsKey(to)) {
			me.endEvents.get(to).add(event);
			return;
		}
		List<GameEvent> list = new LinkedList<GameEvent>();
		list.add(event);
		me.endEvents.put(to, list);
	}
	
	public static void addEndEvent(OnGoingThing to, GameEvent event) {
		if (me.endEvents.containsKey(to)) {
			me.endEvents.get(to).add(event);
			return;
		}
		List<GameEvent> list = new LinkedList<GameEvent>();
		list.add(event);
		me.endEvents.put(to, list);
	}
	
	public static boolean isHappening(Class thing) {
		synchronized (objectLock("Checking for happening")) {
			for (OnGoingThing t : me.onGoingThingList) {
				if (t.getClass() == thing)
					return true;
			}
		}
		return false;
	}
	
	public static void modTranslate(double x, double y) {
		me.translateX += x;
		me.translateY += y;
	}
	
	public static void setTranslateX(double x) {
		me.translateX = x;
	}
	
	public static void setTranslateY(double y) {
		me.translateY = y;
	}
	
	public static boolean getFocusOnPlayer() {
		return me.focusOnPlayer;
	}
	
	public static void setFocusOnPlayer(boolean focus) {
		if (!focus && me.focusOnPlayer) {
			setTranslateX(getTranslateX());
			setTranslateY(getTranslateY());
		}
		me.focusOnPlayer = focus;
	}

	public static Object objectLock(String desc) {
		// System.out.println("Locking for " + desc);
		return me.objectLockSecret;
	}

	public static void registerId(String id, GameObject o) {
		if (id == null || id.length() == 0)
			return;
		if (o.getId().length() > 0) {
			me.objectTable.remove(o.getId());
		}

		me.objectTable.put(id, o);
		o.setId(id);
	}

	public static GameObject getObject(String id) {
		return me.objectTable.get(id);
	}

	/**
	 * Constant representing "Main" objects (normal game objects like the npcs,
	 * geometry, the player, etc)
	 */
	public static final int OBJECT_MAIN = 1;
	/** Constant representing background objects (scenery, etc) */
	public static final int OBJECT_BACK = 2;
	/** Constant representing foreground objects (effects, etc) */
	public static final int OBJECT_FORE = 4;

	public static void clearObjects() {
		synchronized (objectLock("clearObjects")) {
			me.background.clear();
			me.effects.clear();
			me.objects.clear();
		}
	}

	public static void resetState() {
		clearObjects();
		me.onGoingThingList.clear();
		me.blockRegenFor = 0;
		me.blockRegenFlag = false;
	}

	/**
	 * Returns the first object of the specified type which contains the
	 * specified x,y coordinates.
	 * 
	 * @param x
	 * @param y
	 * @param types
	 *            - main, background, or foreground - OBJECT_MAIN, OBJET_BACK,
	 *            OBJECT_FORE
	 * @return
	 */
	public static GameObject getObjectAt(double x, double y, int types) {
		synchronized (objectLock("Get object")) {
			if ((types & OBJECT_MAIN) == OBJECT_MAIN) {
				for (GameObject o : me.objects) {
					if (o.contains(x, y))
						return o;
				}
			}

			if ((types & OBJECT_FORE) == OBJECT_FORE) {
				for (GameObject o : me.effects) {
					if (o.contains(x, y))
						return o;
				}
			}

			if ((types & OBJECT_BACK) == OBJECT_BACK) {
				for (GameObject o : me.background) {
					if (o.contains(x, y))
						return o;
				}
			}
		}

		return null;
	}

	/**
	 * Adds a new TextEffect to the lits of effects. Eg, using
	 * addTextEffect("Foobar", "dev") would add a new TextEffect displaying the
	 * text "Foobar" with the "texteffect.dev" template.
	 * 
	 * @param text
	 *            - The text to be displayed
	 * @param type
	 *            - The template to be used (defined in settings.txt).
	 * @return
	 */
	public static TextEffect addTextEffect(String text, String type) {
		double x = getTranslateX() + 100;
		double y = getTranslateY() + 100;
		
		Actor player = TypeJump.getInst().getPlayer();
		if (player != null) {
			x = player.centerX();
			y = player.bottom();
	
			String sx = Settings.get("texteffect." + type + ".posx");
			if (sx.equals("center")) {
				x = player.centerY();
			} else if (sx.equals("left")) {
				x = player.left();
			} else if (sx.equals("right")) {
				x = player.right();
			}
	
			String sy = Settings.get("texteffect." + type + ".posy");
			if (sy.equals("center")) {
				y = player.centerY();
			} else if (sy.equals("top")) {
				y = player.top();
			} else if (sy.equals("bottom")) {
				y = player.bottom();
			}
		}
		
		TextEffect tc = TextEffect.createFromSettings("texteffect." + type,
				text, x, y);
		GameEngine.addEffect(tc);

		return tc;
	}

	/**
	 * Returns the first object it finds at the given x, y coordinates (includes
	 * background and foreground objects as well)
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public static GameObject getObjectAt(double x, double y) {
		return getObjectAt(x, y, OBJECT_MAIN | OBJECT_BACK | OBJECT_FORE);
	}

	// Make panning awesomer

	/**
	 * Returns the x-coordinate the screen should be translated by to keep the
	 * player on-screen
	 * 
	 * @return
	 */
	public static int getTranslateX() {
		if (me == null)
			return 0;
		if (me.focusOnPlayer) {
			if (me.player == null)
				return 0;
			return -(int) me.player.centerX();
		}
		return (int) me.translateX;
	}

	/**
	 * Returns the y-coordinate the screens hould be translated by to keep the
	 * player on-screen
	 * 
	 * @return
	 */
	public static int getTranslateY() {
		if (me == null)
			return 0;
		if (me.focusOnPlayer) {
			if (me.player == null)
				return 0;
			return -(int) me.player.centerY();
		}
		return (int) me.translateY;
	}

	/**
	 * Blocks swag regeneration for the next frame.
	 */
	public static void blockSwagRegen() {
		me.blockRegenFlag = true;
	}

	/**
	 * Blocks swag regeneration for the next [frames] frames.
	 * 
	 * @param frames
	 */
	public static synchronized void blockSwagRegen(int frames) {
		me.blockRegenFor = Math.max(frames, me.blockRegenFor);
	}

	/**
	 * Adds the given on-going process to the list of processes to run each
	 * frame. This will not add duplicates.
	 * 
	 * @param thing
	 */
	public static void addOnGoingThing(OnGoingThing thing) {
		if (!me.onGoingThingList.contains(thing))
			me.onGoingThingList.add(thing);
	}
	
	public static boolean triggerEvent(GameEvent event) {
		Object obj;
		String mname;
		if (event == null || (obj = event.getObject()) == null
				|| (mname = event.getMethod()) == null)
			return false;
		
		try {
			Method m = obj.getClass().getMethod(mname);
			m.invoke(obj);
		} catch (Exception e) {
			return false;
		}
		
		return true;
	}

	/**
	 * Removes the on-going process, if it exists
	 * 
	 * @param thing
	 */
	public static void removeOnGoingThing(OnGoingThing thing) {
		if (me.onGoingThingList.remove(thing) && me.endEvents.containsKey(thing)) {
			List<GameEvent> ls = me.endEvents.get(thing);
			if (ls != null) {
				while (ls.size() > 0) 
					triggerEvent(ls.remove(0));
			}
		}
	}

	/**
	 * Returns the last player that was added to the engine.
	 * 
	 * @return
	 */
	public static Player getPlayer() {
		return me.player;
	}

	/**
	 * Adds the String to the list of commands to be run by CommandManager
	 * 
	 * @param command
	 */
	public static void addCommand(String command) {
		me.commands.addCommand(command);
	}

	/**
	 * Returns the lock that should be synchronized with when iterating over the
	 * objects for some reason (like painting)
	 * 
	 * @return
	 */
	public static Object getObjectLock(String desc) {
		return objectLock(desc);
	}

	/**
	 * Returns an Iterable of all the game objects, including effects and those
	 * in the background
	 * 
	 * @return
	 */
	public static Iterable<GameObject> getObjects() {
		return me.allObjects;
	}

	/**
	 * Adds this GameObject to the list of objects
	 * 
	 * @param object
	 */
	public static void addObject(GameObject object) {
		synchronized (objectLock("Add Object")) {
			if (object instanceof Player) {
				if (me.player != null)
					removeObject(me.player);
				me.player = (Player) object;
			}
			me.objects.add(object);
		}
	}

	/**
	 * Adds this GameObject to the list of effects objects
	 * 
	 * @param object
	 */
	public static void addEffect(GameObject object) {
		synchronized (objectLock("Add Effect " + object)) {
			me.effects.add(object);
		}
	}

	/**
	 * Adds the GameObject to the list of background objects
	 * 
	 * @param object
	 */
	public static void addBackground(GameObject object) {
		synchronized (objectLock("Add background " + object)) {
			me.background.add(object);
		}
	}

	/**
	 * Removes the given game object
	 * 
	 * @param object
	 */
	public static void removeObject(GameObject object) {
		object.remove();
		synchronized (objectLock("Remove " + object)) {
			me.objects.remove(object);
			me.background.remove(object);
			me.effects.remove(object);
		}
	}

	/**
	 * Starts the game loop thread (only call this once!)
	 */
	public static void start() {
		me.gameThread.start();
	}

	/**
	 * Returns the latest frames per second
	 * 
	 * @return
	 */
	public static double getFPS() {
		if (me.secs == 0)
			return 0;
		return 1 / me.secs;
	}

	private long msStart = 0;
	private long msEnd = 0;
	private long msPass = 0;
	private double secs = 0;

	/**
	 * This is the method that drives <i>everything</i>.
	 */
	private void mainLoop() {
		boolean first = true;

		while (true) {
			msEnd = System.currentTimeMillis();
			msPass = msEnd - msStart;
			msStart = System.currentTimeMillis();
			secs = msPass / 1000.;

			if (first) {
				msPass = 0;
				secs = 0;
				first = false;
			}

			if (!isPaused()) {
				// kills expired messages
				Messenger.tick();
				// removes objects that were flagged for deletion
				removeDeadThings();
				// updates all the objects
				updateObjects(secs);
				// tests for collision
				collisionDetection();
				// processes the text commands
				me.commands.processCommands();
				// simulates on-going game processes
				onGoingThings(secs);
				// regenerates swag
				regenSwag(secs);
			}
		}
	}

	private double syncSettingsFrequency = 1;
	private double syncSettingsCounter = syncSettingsFrequency;

	private void updateSyncCounter(double secs) {
		syncSettingsCounter -= secs;
		if (syncSettingsCounter <= 0) {
			syncSettingsCounter = syncSettingsFrequency;
			Settings.reload();
		}
	}

	private LinkedList<OnGoingThing> happenedThings = new LinkedList<OnGoingThing>();

	private void onGoingThings(double seconds) {
		synchronized (objectLock("Run OnGoingThings")) {
			happenedThings.clear();
			for (OnGoingThing thing : onGoingThingList) {
				if (thing.happen(TypeJump.getInst(), seconds)) {
					happenedThings.add(thing);
				}
			}
			for (OnGoingThing thing : happenedThings)
				removeOnGoingThing(thing);
		}
	}

	private void regenSwag(double secs) {
		if (!blockRegenFlag && blockRegenFor <= 0)
			Swagger.getInst().restoreSwag(
					Swagger.getInst().getMaxSwag() * TypeJump.SWAG_REGEN_RATE
							/ 100. * secs);
		blockRegenFlag = false;
		if (blockRegenFor > 0)
			blockRegenFor--;
	}

	private void updateObjects(double secs) {
		synchronized (objectLock("Update Objects")) {
			for (GameObject o : getObjects()) {
				o.update(secs);
			}
		}
	}

	private void collisionDetectionPass(int pass) {
		synchronized (objectLock("Collision Pass " + pass)) {
			for (GameObject A : objects) {
				if (!A.isPhysical() || !A.isDynamic() || A.shouldRemove())
					continue;
				for (GameObject B : objects) {
					if (A == B)
						continue;
					if (!B.isPhysical() || B.shouldRemove())
						continue;
					if (!A.canHit(B))
						continue;

					if (A.intersects(B)) {
						A.collide(B, pass);
						B.collide(A, pass);
					}
				}
			}
		}
	}

	private void collisionDetection() {
		synchronized (objectLock("Detect Collision")) {
			// The pass # is witchcraft to prevent things like glitchy wall
			// jumping.
			// Most objects only do anything on the 0th pass and ignore it
			// subsequent times.
			collisionDetectionPass(0);
			collisionDetectionPass(1);
			collisionDetectionPass(2);
		}
	}

	private LinkedList<GameObject> toRemove = new LinkedList<GameObject>();

	private void removeDeadThings() {
		synchronized (objectLock("removeDeadThings")) {

			for (GameObject o : allObjects) {
				if (o.shouldRemove()) {
					toRemove.add(o);
				}
			}

			for (GameObject o : toRemove) {
				removeObject(o);
			}

			toRemove.clear();
		}
	}
}
